#!/usr/bin/env perl
#
# Copyright (c) 2012-2013 Jeffrey M. Squyres.  All rights reserved.
#
# $COPYRIGHT$
# 
# Additional copyrights may follow
# 
# $HEADER$
#

package RubySlippers::Engine;

use strict;
use RubySlippers::Messages;
use RubySlippers::Globals;
use RubySlippers::Config;

#
# List of events that are cycling through the engine
#
my $_events;


#
# Load modules of a specific type and list of names
#
sub _load_modules {
    my $name = shift;
    my @plugins = @_;

    # Try to load each plugin, and if successful, run its "open"
    # function to query to see if that plugin wants to run.  If it
    # does, it'll return one or more modules.
    my @ret;
    foreach my $q (@plugins) {
        # Try to load the plugin
        Debug("Loading RubySlippers::${name}::$q");
        my $ret;
        eval "\$ret = require(\"RubySlippers/$name/$q.pm\");";
        if ($@) {
            Debug("Failed to load RubySlippers::${name}::$q -- skipped ($@)");
            next;
        } elsif (1 != $ret) {
            Debug("Failed to load RubySlippers::${name}::$q -- skipped (nonzero return value: $ret");
            next;
        }

        # Run the "open" function on the plugin and get back an array
        # of modules if the plugin wants to run.
        Debug("Opening RubySlippers::${name}::$q");
        my $modules_ref;
        eval "\$modules_ref = RubySlippers::${name}::${q}::open();";

        # Did we get any modules back?
        if (!defined($modules_ref)) {
            Debug("RubySlipers::${name}::${q}::open() returned no modules");
            next;
        }
        my @modules_array = @{$modules_ref};
        if ($#modules_array < 0) {
            Debug("RubySlipers::${name}::${q}::open() returned no modules");
            next;
        }

        # We got some modules back; save them.
        Debug("RubySlipers::${name}::${q}::open() returned " . ($#modules_array + 1) . " modules");
        foreach my $m (@modules_array) {
            my $item = {
                name => $q,
                module => $m,
            };

            if (defined($m->{frequency}) && defined($m->{callback})) {
                Debug("Scheduling $name $q module for $m->{frequency}s from now");
                add_event($m->{callback}, 
                          time() + $m->{frequency}, 
                          $m->{frequency},
                          $m, "Module: $m->{name}");
            }
            push(@ret, $item);
        }
    }

    # If we got some modules, log a message
    if ($#ret >= 0) {
        my $logstr = "Found the following $name modules: ";
        my $sep;
        foreach my $m (@ret) {
            $logstr .= $sep . $m->{name};
            $sep = ", ";
        }
        Verbose($logstr);
    } else {
        Verbose("Found no $name modules!");
    }

    # Return the modules we found
    return @ret;
}

#
# From the list of items, find the one with the next timeout
#
sub _find_next_timeout {
    my $list = shift;

    # If we can't find a timestamp within the next year, something is
    # wrong
    my $next_timestamp = time() + (365 * 24 * 60 * 60);
    my $next_item;
    my $t = time;

    # Find the item with the next timestamp that expires
    foreach my $item (@{$list}) {
        if ($item->{timestamp_next_call} < $next_timestamp) {
            $next_timestamp = $item->{timestamp_next_call};
            $next_item = \$item;
        }
    }

    Debug("Next timeout is $next_timestamp, ${$next_item}->{name}");
    return ($next_item, $next_timestamp - $t);
}

#
# Unload modules
#
sub _unload_modules {
    my $name = shift;

    Debug("Unloading/closing $name modules");
    foreach my $item (@_) {
        if (defined($item->{module}->{close})) {
            Debug("Unloading/closing $item->{name}");
            $item->{module}->{close}();
        }
    }
    Debug("Unloaded/closed all modules");
}

#
# Add an event to be run by the engine.  Repeating events should pass
# a frequency>0, and will be automatically re-scheduled after they are
# triggered.  Non-repeating events should set a frequency of 0, and
# will automatically be deleted when after they are triggered.
#
sub add_event {
    my ($func, $time, $frequency, $state, $name) = @_;

    # Ensure that we have no collisions
    while (defined($_events->{$time})) {
        $time += .0001;
    }

    # Add the event to the hash
    Debug("Adding $name event for time $time");
    $_events->{$time} = {
        func => $func,
        time => $time,
        frequency => $frequency,
        state => $state,
        name => $name,
    };
}

#
# Remove the first instance of a named event from the events list
#
sub remove_event {
    my $name = shift;

    foreach my $t (sort(keys(%{$_events}))) {
        if ($_events->{$t}->{name} eq $name) {

            my $ret = $_events->{$t};
            delete $_events->{$t};
            return $ret;
        }
    }

    return undef;
}

#
# Main engine loop
#
sub _main_loop {
    Verbose("Entering main engine loop");

    while (1) {
        my @k = sort(keys(%{$_events}));

        # If there's nothing to run, bail
        if ($#k < 0) {
            Debug("No modules available / negative timeout, time to exit the main loop");
            last;
        }

        my $time_to_run = $k[0];
        if ($time_to_run > time()) {
            my $timeout = $time_to_run - time();
            Debug("Sleeping for $timeout seconds...");
            sleep($timeout);
        }

        # Remove the event from the events list
        my $event = remove_event($_events->{$time_to_run}->{name});
        die "This shouldn't happen"
            if (!defined($event));

        # Now run the event
        Debug("Calling $event->{name} event");
        my $ret = $event->{func}($event->{state});

        # If the event we just ran has a frequency>0, re-schedule it
        # for the future.  Otherwise, it was a "one-shot" event that
        # doesn't need to be re-scheduled.
        if (defined($event->{frequency}) && $event->{frequency} > 0) {
            add_event($event->{func}, time() + $event->{frequency},
                      $event->{frequency}, $event->{state}, $event->{name});
        }
    }
}

#
# Main entry point for the Engine
#
sub run {
    my ($foreground, $debug, $config) = @_;

    # Load the config
    RubySlippers::Config::load_config($config);

    # If we're not running in the foreground, fork a child into the
    # background
    if ($foreground) {
        Verbose("Running in the foreground");
    } else {
        my $pid = fork();
        die "Failed to daemonize process" 
            if ($pid < 0);

        # If we're the parent, just return
        return 0
            if ($pid != 0);

        # If we're the child, close up the normal file descriptors so
        # that we can truly be free
        close(STDIN);
        close(STDOUT);
        close(STDERR);

        # Let the parent die.  There's some weirdness (perhaps a perl
        # bug?)  that sometimes causes the child to hang when calling
        # system() if you don't let the parent die nicely.  :-( At
        # least on the version of Perl that's on Raspian.  :-\
        sleep(1);
        Verbose("Running as daemon");
    }

    # Load up query modules (hard-coded names for now)
    @RubySlippers::Globals::query_modules = 
        _load_modules("Query", qw/MDNS/);

    # Load up decision modules
    @RubySlippers::Globals::decision_modules = 
        _load_modules("Decision", qw/Time/);

    # Load up action modules
    @RubySlippers::Globals::action_modules = 
        _load_modules("Action", qw/Nest Pushover/);

    # Ensure that we have at least one of each type of module
    my $ret = 0;
    if ($#RubySlippers::Globals::query_modules < 0) {
        Verbose("No Query modules available; quitting");
        $ret = 1;
    }
    if ($#RubySlippers::Globals::decision_modules < 0) {
        Verbose("No Decision modules available; quitting");
        $ret = 1;
    }
    if ($#RubySlippers::Globals::action_modules < 0) {
        Verbose("No Action modules available; quitting");
        $ret = 1;
    }

    # Launch the main engine loop if everything above succeeded.  If
    # we enter the main loop, it won't return.
    if (0 == $ret) {
        _main_loop();
        Debug("Engine::main_loop() unexpectedly returned!");
    }

    # We're done -- close/unload modules
    _unload_modules("Query", @RubySlippers::Globals::query_modules);
    _unload_modules("Decision", @RubySlippers::Globals::decision_modules);
    _unload_modules("Action", @RubySlippers::Globals::action_modules);

    return $ret;
}

#
# This function is called by Query modules when they have something
# they want to report.
#
sub Decide {
    Debug("Decide");
    foreach my $module (@RubySlippers::Globals::decision_modules) {
        $module->{module}->{decide}(@_);
    }
}

#
# This function is called by Decision modules when they have something
# they want to report.
#
sub Act {
    Debug("Act");
    foreach my $module (@RubySlippers::Globals::action_modules) {
        $module->{module}->{act}(@_);
    }
}

1;
